{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "#skip\n",
    "! [ -e /content ] && pip install -Uqq fastai  # upgrade fastai on colab"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#default_exp data.transforms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "from fastai.torch_basics import *\n",
    "from fastai.data.core import *\n",
    "from fastai.data.load import *\n",
    "from fastai.data.external import *\n",
    "\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "import posixpath"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "from nbdev.showdoc import *"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Helper functions for processing data and basic transforms\n",
    "\n",
    "> Functions for getting, splitting, and labeling data, as well as generic transforms"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Get, split, and label"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For most data source creation we need functions to get a list of items, split them in to train/valid sets, and label them. fastai provides functions to make each of these steps easy (especially when combined with `fastai.data.blocks`)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Get"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First we'll look at functions that *get* a list of items (generally file names).\n",
    "\n",
    "We'll use *tiny MNIST* (a subset of MNIST with just two classes, `7`s and `3`s) for our examples/tests throughout this page."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(#2) [Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/7')]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "path = untar_data(URLs.MNIST_TINY)\n",
    "(path/'train').ls()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def _get_files(p, fs, extensions=None):\n",
    "    p = Path(p)\n",
    "    res = [p/f for f in fs if not f.startswith('.')\n",
    "           and ((not extensions) or f'.{f.split(\".\")[-1].lower()}' in extensions)]\n",
    "    return res"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def get_files(path, extensions=None, recurse=True, folders=None, followlinks=True):\n",
    "    \"Get all the files in `path` with optional `extensions`, optionally with `recurse`, only in `folders`, if specified.\"\n",
    "    path = Path(path)\n",
    "    folders=L(folders)\n",
    "    extensions = setify(extensions)\n",
    "    extensions = {e.lower() for e in extensions}\n",
    "    if recurse:\n",
    "        res = []\n",
    "        for i,(p,d,f) in enumerate(os.walk(path, followlinks=followlinks)): # returns (dirpath, dirnames, filenames)\n",
    "            if len(folders) !=0 and i==0: d[:] = [o for o in d if o in folders]\n",
    "            else:                         d[:] = [o for o in d if not o.startswith('.')]\n",
    "            if len(folders) !=0 and i==0 and '.' not in folders: continue\n",
    "            res += _get_files(p, f, extensions)\n",
    "    else:\n",
    "        f = [o.name for o in os.scandir(path) if o.is_file()]\n",
    "        res = _get_files(path, f, extensions)\n",
    "    return L(res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is the most general way to grab a bunch of file names from disk. If you pass `extensions` (including the `.`) then returned file names are filtered by that list. Only those files directly in `path` are included, unless you pass `recurse`, in which case all child folders are also searched recursively. `folders` is an optional list of directories to limit the search to."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(#709) [Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/9427.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/7043.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/9868.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/9386.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/8771.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/9916.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/7618.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/890.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/8360.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/8830.png')...]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t3 = get_files(path/'train'/'3', extensions='.png', recurse=False)\n",
    "t7 = get_files(path/'train'/'7', extensions='.png', recurse=False)\n",
    "t  = get_files(path/'train', extensions='.png', recurse=True)\n",
    "test_eq(len(t), len(t3)+len(t7))\n",
    "test_eq(len(get_files(path/'train'/'3', extensions='.jpg', recurse=False)),0)\n",
    "test_eq(len(t), len(get_files(path, extensions='.png', recurse=True, folders='train')))\n",
    "t"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "test_eq(len(get_files(path/'train'/'3', recurse=False)),346)\n",
    "test_eq(len(get_files(path, extensions='.png', recurse=True, folders=['train', 'test'])),729)\n",
    "test_eq(len(get_files(path, extensions='.png', recurse=True, folders='train')),709)\n",
    "test_eq(len(get_files(path, extensions='.png', recurse=True, folders='training')),0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It's often useful to be able to create functions with customized behavior. `fastai.data` generally uses functions named as CamelCase verbs ending in `er` to create these functions. `FileGetter` is a simple example of such a function creator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def FileGetter(suf='', extensions=None, recurse=True, folders=None):\n",
    "    \"Create `get_files` partial function that searches path suffix `suf`, only in `folders`, if specified, and passes along args\"\n",
    "    def _inner(o, extensions=extensions, recurse=recurse, folders=folders):\n",
    "        return get_files(o/suf, extensions, recurse, folders)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fpng = FileGetter(extensions='.png', recurse=False)\n",
    "test_eq(len(t7), len(fpng(path/'train'/'7')))\n",
    "test_eq(len(t), len(fpng(path/'train', recurse=True)))\n",
    "fpng_r = FileGetter(extensions='.png', recurse=True)\n",
    "test_eq(len(t), len(fpng_r(path/'train')))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "image_extensions = set(k for k,v in mimetypes.types_map.items() if v.startswith('image/'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def get_image_files(path, recurse=True, folders=None):\n",
    "    \"Get image files in `path` recursively, only in `folders`, if specified.\"\n",
    "    return get_files(path, extensions=image_extensions, recurse=recurse, folders=folders)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is simply `get_files` called with a list of standard image extensions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(len(t), len(get_image_files(path, recurse=True, folders='train')))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def ImageGetter(suf='', recurse=True, folders=None):\n",
    "    \"Create `get_image_files` partial that searches suffix `suf` and passes along `kwargs`, only in `folders`, if specified\"\n",
    "    def _inner(o, recurse=recurse, folders=folders): return get_image_files(o/suf, recurse, folders)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Same as `FileGetter`, but for image extensions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(len(get_files(path/'train', extensions='.png', recurse=True, folders='3')),\n",
    "        len(ImageGetter(   'train',                    recurse=True, folders='3')(path)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def get_text_files(path, recurse=True, folders=None):\n",
    "    \"Get text files in `path` recursively, only in `folders`, if specified.\"\n",
    "    return get_files(path, extensions=['.txt'], recurse=recurse, folders=folders)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class ItemGetter(ItemTransform):\n",
    "    \"Creates a proper transform that applies `itemgetter(i)` (even on a tuple)\"\n",
    "    _retain = False\n",
    "    def __init__(self, i): self.i = i\n",
    "    def encodes(self, x): return x[self.i]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(ItemGetter(1)((1,2,3)),  2)\n",
    "test_eq(ItemGetter(1)(L(1,2,3)), 2)\n",
    "test_eq(ItemGetter(1)([1,2,3]),  2)\n",
    "test_eq(ItemGetter(1)(np.array([1,2,3])),  2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class AttrGetter(ItemTransform):\n",
    "    \"Creates a proper transform that applies `attrgetter(nm)` (even on a tuple)\"\n",
    "    _retain = False\n",
    "    def __init__(self, nm, default=None): store_attr()\n",
    "    def encodes(self, x): return getattr(x, self.nm, self.default)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(AttrGetter('shape')(torch.randn([4,5])), [4,5])\n",
    "test_eq(AttrGetter('shape', [0])([4,5]), [0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Split"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The next set of functions are used to *split* data into training and validation sets. The functions return two lists - a list of indices or masks for each of training and validation sets."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def RandomSplitter(valid_pct=0.2, seed=None):\n",
    "    \"Create function that splits `items` between train/val with `valid_pct` randomly.\"\n",
    "    def _inner(o):\n",
    "        if seed is not None: torch.manual_seed(seed)\n",
    "        rand_idx = L(list(torch.randperm(len(o)).numpy()))\n",
    "        cut = int(valid_pct * len(o))\n",
    "        return rand_idx[cut:],rand_idx[:cut]\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def _test_splitter(f, items=None):\n",
    "    \"A basic set of condition a splitter must pass\"\n",
    "    items = ifnone(items, range_of(30))\n",
    "    trn,val = f(items)\n",
    "    assert 0<len(trn)<len(items)\n",
    "    assert all(o not in val for o in trn)\n",
    "    test_eq(len(trn), len(items)-len(val))\n",
    "    # test random seed consistency\n",
    "    test_eq(f(items)[0], trn)\n",
    "    return trn, val"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((#24) [10,18,16,23,28,26,20,7,21,22...], (#6) [12,0,6,25,8,15])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "_test_splitter(RandomSplitter(seed=42))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use scikit-learn train_test_split. This allow to *split* items in a stratified fashion (uniformely according to the ‘labels‘ distribution)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def TrainTestSplitter(test_size=0.2, random_state=None, stratify=None, train_size=None, shuffle=True):\n",
    "    \"Split `items` into random train and test subsets using sklearn train_test_split utility.\"\n",
    "    def _inner(o, **kwargs):\n",
    "        train,valid = train_test_split(range_of(o), test_size=test_size, random_state=random_state,\n",
    "                                        stratify=stratify, train_size=train_size, shuffle=shuffle)\n",
    "        return L(train), L(valid)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "src = list(range(30))\n",
    "labels = [0] * 20 + [1] * 10\n",
    "test_size = 0.2\n",
    "\n",
    "f = TrainTestSplitter(test_size=test_size, random_state=42, stratify=labels)\n",
    "trn,val = _test_splitter(f)\n",
    "\n",
    "# test labels distribution consistency\n",
    "# there should be test_size % of zeroes and ones respectively in the validation set\n",
    "test_eq(len([t for t in val if t < 20]) / 20, test_size)\n",
    "test_eq(len([t for t in val if t > 20]) / 10, test_size)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def IndexSplitter(valid_idx):\n",
    "    \"Split `items` so that `val_idx` are in the validation set and the others in the training set\"\n",
    "    def _inner(o):\n",
    "        train_idx = np.setdiff1d(np.array(range_of(o)), np.array(valid_idx))\n",
    "        return L(train_idx, use_list=True), L(valid_idx, use_list=True)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = 'a,b,c,d,e,f,g,h,i,j'.split(',')  #to make obvious that splits indexes and not items.\n",
    "splitter = IndexSplitter([3,7,9])\n",
    "\n",
    "_test_splitter(splitter, items)\n",
    "test_eq(splitter(items),[[0,1,2,4,5,6,8],[3,7,9]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def EndSplitter(valid_pct=0.2, valid_last=True):\n",
    "    \"Create function that splits `items` between train/val with `valid_pct` at the end if `valid_last` else at the start. Useful for ordered data.\"\n",
    "    assert 0<valid_pct<1, \"valid_pct must be in (0,1)\"\n",
    "    def _inner(o):\n",
    "        idxs = range_of(o)\n",
    "        cut = int(valid_pct * len(o))\n",
    "        return (idxs[:-cut], idxs[-cut:]) if valid_last else (idxs[cut:],idxs[:cut])\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = range_of(10)\n",
    "\n",
    "splitter_last = EndSplitter(valid_last=True)\n",
    "_test_splitter(splitter_last)\n",
    "test_eq(splitter_last(items), ([0,1,2,3,4,5,6,7], [8,9]))\n",
    "\n",
    "splitter_start = EndSplitter(valid_last=False)\n",
    "_test_splitter(splitter_start)\n",
    "test_eq(splitter_start(items), ([2,3,4,5,6,7,8,9], [0,1]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def _grandparent_idxs(items, name):\n",
    "    def _inner(items, name): return mask2idxs(Path(o).parent.parent.name == name for o in items)\n",
    "    return [i for n in L(name) for i in _inner(items,n)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def GrandparentSplitter(train_name='train', valid_name='valid'):\n",
    "    \"Split `items` from the grand parent folder names (`train_name` and `valid_name`).\"\n",
    "    def _inner(o):\n",
    "        return _grandparent_idxs(o, train_name),_grandparent_idxs(o, valid_name)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fnames = [path/'train/3/9932.png', path/'valid/7/7189.png', \n",
    "          path/'valid/7/7320.png', path/'train/7/9833.png',  \n",
    "          path/'train/3/7666.png', path/'valid/3/925.png',\n",
    "          path/'train/7/724.png', path/'valid/3/93055.png']\n",
    "splitter = GrandparentSplitter()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_test_splitter(splitter, items=fnames)\n",
    "test_eq(splitter(fnames),[[0,3,4,6],[1,2,5,7]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fnames2 = fnames + [path/'test/3/4256.png', path/'test/7/2345.png', path/'valid/7/6467.png']\n",
    "splitter = GrandparentSplitter(train_name=('train', 'valid'), valid_name='test')\n",
    "_test_splitter(splitter, items=fnames2)\n",
    "test_eq(splitter(fnames2),[[0,3,4,6,1,2,5,7,10],[8,9]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def FuncSplitter(func):\n",
    "    \"Split `items` by result of `func` (`True` for validation, `False` for training set).\"\n",
    "    def _inner(o):\n",
    "        val_idx = mask2idxs(func(o_) for o_ in o)\n",
    "        return IndexSplitter(val_idx)(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "splitter = FuncSplitter(lambda o: Path(o).parent.parent.name == 'valid')\n",
    "_test_splitter(splitter, fnames)\n",
    "test_eq(splitter(fnames),[[0,3,4,6],[1,2,5,7]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def MaskSplitter(mask):\n",
    "    \"Split `items` depending on the value of `mask`.\"\n",
    "    def _inner(o): return IndexSplitter(mask2idxs(mask))(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = list(range(6))\n",
    "splitter = MaskSplitter([True,False,False,True,False,True])\n",
    "_test_splitter(splitter, items)\n",
    "test_eq(splitter(items),[[1,2,4],[0,3,5]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def FileSplitter(fname):\n",
    "    \"Split `items` by providing file `fname` (contains names of valid items separated by newline).\"\n",
    "    valid = Path(fname).read_text().split('\\n')\n",
    "    def _func(x): return x.name in valid\n",
    "    def _inner(o): return FuncSplitter(_func)(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with tempfile.TemporaryDirectory() as d:\n",
    "    fname = Path(d)/'valid.txt'\n",
    "    fname.write_text('\\n'.join([Path(fnames[i]).name for i in [1,3,4]]))\n",
    "    splitter = FileSplitter(fname)\n",
    "    _test_splitter(splitter, fnames)\n",
    "    test_eq(splitter(fnames),[[0,2,5,6,7],[1,3,4]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def ColSplitter(col='is_valid'):\n",
    "    \"Split `items` (supposed to be a dataframe) by value in `col`\"\n",
    "    def _inner(o):\n",
    "        assert isinstance(o, pd.DataFrame), \"ColSplitter only works when your items are a pandas DataFrame\"\n",
    "        valid_idx = (o.iloc[:,col] if isinstance(col, int) else o[col]).values.astype('bool')\n",
    "        return IndexSplitter(mask2idxs(valid_idx))(o)\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame({'a': [0,1,2,3,4], 'b': [True,False,True,True,False]})\n",
    "splits = ColSplitter('b')(df)\n",
    "test_eq(splits, [[1,4], [0,2,3]])\n",
    "#Works with strings or index\n",
    "splits = ColSplitter(1)(df)\n",
    "test_eq(splits, [[1,4], [0,2,3]])\n",
    "# does not get confused if the type of 'is_valid' is integer, but it meant to be a yes/no\n",
    "df = pd.DataFrame({'a': [0,1,2,3,4], 'is_valid': [1,0,1,1,0]})\n",
    "splits_by_int = ColSplitter('is_valid')(df)\n",
    "test_eq(splits_by_int, [[1,4], [0,2,3]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def RandomSubsetSplitter(train_sz, valid_sz, seed=None):\n",
    "    \"Take randoms subsets of `splits` with `train_sz` and `valid_sz`\"\n",
    "    assert 0 < train_sz < 1\n",
    "    assert 0 < valid_sz < 1\n",
    "    assert train_sz + valid_sz <= 1.\n",
    "\n",
    "    def _inner(o):\n",
    "        if seed is not None: torch.manual_seed(seed)\n",
    "        train_len,valid_len = int(len(o)*train_sz),int(len(o)*valid_sz)\n",
    "        idxs = L(list(torch.randperm(len(o)).numpy()))\n",
    "        return idxs[:train_len],idxs[train_len:train_len+valid_len]\n",
    "    return _inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = list(range(100))\n",
    "valid_idx = list(np.arange(70,100))\n",
    "splitter = RandomSubsetSplitter(0.3, 0.1)\n",
    "splits = RandomSubsetSplitter(0.3, 0.1)(items)\n",
    "test_eq(len(splits[0]), 30)\n",
    "test_eq(len(splits[1]), 10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Label"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The final set of functions is used to *label* a single item of data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def parent_label(o):\n",
    "    \"Label `item` with the parent folder name.\"\n",
    "    return Path(o).parent.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that `parent_label` doesn't have anything customize, so it doesn't return a function - you can just use it directly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['3', '7', '7', '7', '3', '3', '7', '3']"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "test_eq(parent_label(fnames[0]), '3')\n",
    "test_eq(parent_label(\"fastai_dev/dev/data/mnist_tiny/train/3/9932.png\"), '3')\n",
    "[parent_label(o) for o in fnames]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "#test for MS Windows when os.path.sep is '\\\\' instead of '/'\n",
    "test_eq(parent_label(os.path.join(\"fastai_dev\",\"dev\",\"data\",\"mnist_tiny\",\"train\", \"3\", \"9932.png\") ), '3')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class RegexLabeller():\n",
    "    \"Label `item` with regex `pat`.\"\n",
    "    def __init__(self, pat, match=False):\n",
    "        self.pat = re.compile(pat)\n",
    "        self.matcher = self.pat.match if match else self.pat.search\n",
    "\n",
    "    def __call__(self, o):\n",
    "        o = str(o).replace(os.sep, posixpath.sep)\n",
    "        res = self.matcher(o)\n",
    "        assert res,f'Failed to find \"{self.pat}\" in \"{o}\"'\n",
    "        return res.group(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`RegexLabeller` is a very flexible function since it handles any regex search of the stringified item. Pass `match=True` to use `re.match` (i.e. check only start of string), or `re.search` otherwise (default).\n",
    "\n",
    "For instance, here's an example the replicates the previous `parent_label` results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['3', '7', '7', '7', '3', '3', '7', '3']"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "f = RegexLabeller(fr'{posixpath.sep}(\\d){posixpath.sep}')\n",
    "test_eq(f(fnames[0]), '3')\n",
    "[f(o) for o in fnames]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['3', '7', '7', '7', '3', '3', '7', '3']"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import posixpath\n",
    "f = RegexLabeller(fr'{posixpath.sep}(\\d){posixpath.sep}')\n",
    "a1 = Path(fnames[0]).as_posix()\n",
    "test_eq(f(a1), '3')\n",
    "[f(o) for o in fnames]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f = RegexLabeller(r'(\\d*)', match=True)\n",
    "test_eq(f(fnames[0].name), '9932')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class ColReader(DisplayedTransform):\n",
    "    \"Read `cols` in `row` with potential `pref` and `suff`\"\n",
    "    def __init__(self, cols, pref='', suff='', label_delim=None):\n",
    "        store_attr()\n",
    "        self.pref = str(pref) + os.path.sep if isinstance(pref, Path) else pref\n",
    "        self.cols = L(cols)\n",
    "\n",
    "    def _do_one(self, r, c):\n",
    "        o = r[c] if isinstance(c, int) else r[c] if c=='name' or c=='cat' else getattr(r, c)\n",
    "        if len(self.pref)==0 and len(self.suff)==0 and self.label_delim is None: return o\n",
    "        if self.label_delim is None: return f'{self.pref}{o}{self.suff}'\n",
    "        else: return o.split(self.label_delim) if len(o)>0 else []\n",
    "\n",
    "    def __call__(self, o, **kwargs):\n",
    "        if len(self.cols) == 1: return self._do_one(o, self.cols[0])\n",
    "        return L(self._do_one(o, c) for c in self.cols)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`cols` can be a list of column names or a list of indices (or a mix of both). If `label_delim` is passed, the result is split using it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame({'a': 'a b c d'.split(), 'b': ['1 2', '0', '', '1 2 3']})\n",
    "f = ColReader('a', pref='0', suff='1')\n",
    "test_eq([f(o) for o in df.itertuples()], '0a1 0b1 0c1 0d1'.split())\n",
    "\n",
    "f = ColReader('b', label_delim=' ')\n",
    "test_eq([f(o) for o in df.itertuples()], [['1', '2'], ['0'], [], ['1', '2', '3']])\n",
    "\n",
    "df['a1'] = df['a']\n",
    "f = ColReader(['a', 'a1'], pref='0', suff='1')\n",
    "test_eq([f(o) for o in df.itertuples()], [L('0a1', '0a1'), L('0b1', '0b1'), L('0c1', '0c1'), L('0d1', '0d1')])\n",
    "\n",
    "df = pd.DataFrame({'a': [L(0,1), L(2,3,4), L(5,6,7)]})\n",
    "f = ColReader('a')\n",
    "test_eq([f(o) for o in df.itertuples()], [L(0,1), L(2,3,4), L(5,6,7)])\n",
    "\n",
    "df['name'] = df['a']\n",
    "f = ColReader('name')\n",
    "test_eq([f(df.iloc[0,:])], [L(0,1)])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Categorize -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class CategoryMap(CollBase):\n",
    "    \"Collection of categories with the reverse mapping in `o2i`\"\n",
    "    def __init__(self, col, sort=True, add_na=False, strict=False):\n",
    "        if is_categorical_dtype(col):\n",
    "            items = L(col.cat.categories, use_list=True)\n",
    "            #Remove non-used categories while keeping order\n",
    "            if strict: items = L(o for o in items if o in col.unique())\n",
    "        else:\n",
    "            if not hasattr(col,'unique'): col = L(col, use_list=True)\n",
    "            # `o==o` is the generalized definition of non-NaN used by Pandas\n",
    "            items = L(o for o in col.unique() if o==o)\n",
    "            if sort: items = items.sorted()\n",
    "        self.items = '#na#' + items if add_na else items\n",
    "        self.o2i = defaultdict(int, self.items.val2idx()) if add_na else dict(self.items.val2idx())\n",
    "\n",
    "    def map_objs(self,objs):\n",
    "        \"Map `objs` to IDs\"\n",
    "        return L(self.o2i[o] for o in objs)\n",
    "\n",
    "    def map_ids(self,ids):\n",
    "        \"Map `ids` to objects in vocab\"\n",
    "        return L(self.items[o] for o in ids)\n",
    "\n",
    "    def __eq__(self,b): return all_equal(b,self)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = CategoryMap([4,2,3,4])\n",
    "test_eq(t, [2,3,4])\n",
    "test_eq(t.o2i, {2:0,3:1,4:2})\n",
    "test_eq(t.map_objs([2,3]), [0,1])\n",
    "test_eq(t.map_ids([0,1]), [2,3])\n",
    "test_fail(lambda: t.o2i['unseen label'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = CategoryMap([4,2,3,4], add_na=True)\n",
    "test_eq(t, ['#na#',2,3,4])\n",
    "test_eq(t.o2i, {'#na#':0,2:1,3:2,4:3})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = CategoryMap(pd.Series([4,2,3,4]), sort=False)\n",
    "test_eq(t, [4,2,3])\n",
    "test_eq(t.o2i, {4:0,2:1,3:2})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "col = pd.Series(pd.Categorical(['M','H','L','M'], categories=['H','M','L'], ordered=True))\n",
    "t = CategoryMap(col)\n",
    "test_eq(t, ['H','M','L'])\n",
    "test_eq(t.o2i, {'H':0,'M':1,'L':2})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "col = pd.Series(pd.Categorical(['M','H','M'], categories=['H','M','L'], ordered=True))\n",
    "t = CategoryMap(col, strict=True)\n",
    "test_eq(t, ['H','M'])\n",
    "test_eq(t.o2i, {'H':0,'M':1})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class Categorize(DisplayedTransform):\n",
    "    \"Reversible transform of category string to `vocab` id\"\n",
    "    loss_func,order=CrossEntropyLossFlat(),1\n",
    "    def __init__(self, vocab=None, sort=True, add_na=False):\n",
    "        if vocab is not None: vocab = CategoryMap(vocab, sort=sort, add_na=add_na)\n",
    "        store_attr()\n",
    "\n",
    "    def setups(self, dsets):\n",
    "        if self.vocab is None and dsets is not None: self.vocab = CategoryMap(dsets, sort=self.sort, add_na=self.add_na)\n",
    "        self.c = len(self.vocab)\n",
    "\n",
    "    def encodes(self, o): \n",
    "        try:\n",
    "            return TensorCategory(self.vocab.o2i[o])\n",
    "        except KeyError as e:\n",
    "            raise KeyError(f\"Label '{o}' was not included in the training dataset\") from e\n",
    "    def decodes(self, o): return Category      (self.vocab    [o])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class Category(str, ShowTitle): _show_args = {'label': 'category'}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = Categorize()\n",
    "tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])\n",
    "test_eq(cat.vocab, ['cat', 'dog'])\n",
    "test_eq(cat('cat'), 0)\n",
    "test_eq(cat.decode(1), 'dog')\n",
    "test_stdout(lambda: show_at(tds,2), 'cat')\n",
    "test_fail(lambda: cat('bird'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = Categorize(add_na=True)\n",
    "tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])\n",
    "test_eq(cat.vocab, ['#na#', 'cat', 'dog'])\n",
    "test_eq(cat('cat'), 1)\n",
    "test_eq(cat.decode(2), 'dog')\n",
    "test_stdout(lambda: show_at(tds,2), 'cat')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = Categorize(vocab=['dog', 'cat'], sort=False, add_na=True)\n",
    "tds = Datasets(['cat', 'dog', 'cat'], tfms=[cat])\n",
    "test_eq(cat.vocab, ['#na#', 'dog', 'cat'])\n",
    "test_eq(cat('dog'), 1)\n",
    "test_eq(cat.decode(2), 'cat')\n",
    "test_stdout(lambda: show_at(tds,2), 'cat')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multicategorize -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class MultiCategorize(Categorize):\n",
    "    \"Reversible transform of multi-category strings to `vocab` id\"\n",
    "    loss_func,order=BCEWithLogitsLossFlat(),1\n",
    "    def __init__(self, vocab=None, add_na=False): super().__init__(vocab=vocab,add_na=add_na,sort=vocab==None)\n",
    "\n",
    "    def setups(self, dsets):\n",
    "        if not dsets: return\n",
    "        if self.vocab is None:\n",
    "            vals = set()\n",
    "            for b in dsets: vals = vals.union(set(b))\n",
    "            self.vocab = CategoryMap(list(vals), add_na=self.add_na)\n",
    "\n",
    "    def encodes(self, o): \n",
    "        if not all(elem in self.vocab.o2i.keys() for elem in o):\n",
    "            diff = [elem for elem in o if elem not in self.vocab.o2i.keys()]\n",
    "            diff_str = \"', '\".join(diff)\n",
    "            raise KeyError(f\"Labels '{diff_str}' were not included in the training dataset\")\n",
    "        return TensorMultiCategory([self.vocab.o2i[o_] for o_ in o])\n",
    "    def decodes(self, o): return MultiCategory      ([self.vocab    [o_] for o_ in o])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class MultiCategory(L):\n",
    "    def show(self, ctx=None, sep=';', color='black', **kwargs):\n",
    "        return show_title(sep.join(self.map(str)), ctx=ctx, color=color, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat = MultiCategorize()\n",
    "tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], tfms=[cat])\n",
    "test_eq(tds[3][0], TensorMultiCategory([]))\n",
    "test_eq(cat.vocab, ['a', 'b', 'c'])\n",
    "test_eq(cat(['a', 'c']), tensor([0,2]))\n",
    "test_eq(cat([]), tensor([]))\n",
    "test_eq(cat.decode([1]), ['b'])\n",
    "test_eq(cat.decode([0,2]), ['a', 'c'])\n",
    "test_stdout(lambda: show_at(tds,2), 'a;c')\n",
    "\n",
    "# if vocab supplied, ensure it maintains its order (i.e., it doesn't sort)\n",
    "cat = MultiCategorize(vocab=['z', 'y', 'x'])\n",
    "test_eq(cat.vocab, ['z','y','x'])\n",
    "\n",
    "test_fail(lambda: cat('bird'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class OneHotEncode(DisplayedTransform):\n",
    "    \"One-hot encodes targets\"\n",
    "    order=2\n",
    "    def __init__(self, c=None): store_attr()\n",
    "\n",
    "    def setups(self, dsets):\n",
    "        if self.c is None: self.c = len(L(getattr(dsets, 'vocab', None)))\n",
    "        if not self.c: warn(\"Couldn't infer the number of classes, please pass a value for `c` at init\")\n",
    "\n",
    "    def encodes(self, o): return TensorMultiCategory(one_hot(o, self.c).float())\n",
    "    def decodes(self, o): return one_hot_decode(o, None)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Works in conjunction with ` MultiCategorize` or on its own if you have one-hot encoded targets (pass a `vocab` for decoding and `do_encode=False` in this case)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_tfm = OneHotEncode(c=3)\n",
    "test_eq(_tfm([0,2]), tensor([1.,0,1]))\n",
    "test_eq(_tfm.decode(tensor([0,1,1])), [1,2])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], [[MultiCategorize(), OneHotEncode()]])\n",
    "test_eq(tds[1], [tensor([1.,0,0])])\n",
    "test_eq(tds[3], [tensor([0.,0,0])])\n",
    "test_eq(tds.decode([tensor([False, True, True])]), [['b','c']])\n",
    "test_eq(type(tds[1][0]), TensorMultiCategory)\n",
    "test_stdout(lambda: show_at(tds,2), 'a;c')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "#test with passing the vocab\n",
    "tds = Datasets([['b', 'c'], ['a'], ['a', 'c'], []], [[MultiCategorize(vocab=['a', 'b', 'c']), OneHotEncode()]])\n",
    "test_eq(tds[1], [tensor([1.,0,0])])\n",
    "test_eq(tds[3], [tensor([0.,0,0])])\n",
    "test_eq(tds.decode([tensor([False, True, True])]), [['b','c']])\n",
    "test_eq(type(tds[1][0]), TensorMultiCategory)\n",
    "test_stdout(lambda: show_at(tds,2), 'a;c')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class EncodedMultiCategorize(Categorize):\n",
    "    \"Transform of one-hot encoded multi-category that decodes with `vocab`\"\n",
    "    loss_func,order=BCEWithLogitsLossFlat(),1\n",
    "    def __init__(self, vocab):\n",
    "        super().__init__(vocab, sort=vocab==None)\n",
    "        self.c = len(vocab)\n",
    "    def encodes(self, o): return TensorMultiCategory(tensor(o).float())\n",
    "    def decodes(self, o): return MultiCategory (one_hot_decode(o, self.vocab))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_tfm = EncodedMultiCategorize(vocab=['a', 'b', 'c'])\n",
    "test_eq(_tfm([1,0,1]), tensor([1., 0., 1.]))\n",
    "test_eq(type(_tfm([1,0,1])), TensorMultiCategory)\n",
    "test_eq(_tfm.decode(tensor([False, True, True])), ['b','c'])\n",
    "\n",
    "_tfm2 = EncodedMultiCategorize(vocab=['c', 'b', 'a'])\n",
    "test_eq(_tfm2.vocab, ['c', 'b', 'a'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class RegressionSetup(DisplayedTransform):\n",
    "    \"Transform that floatifies targets\"\n",
    "    loss_func=MSELossFlat()\n",
    "    def __init__(self, c=None): store_attr()\n",
    "\n",
    "    def encodes(self, o): return tensor(o).float()\n",
    "    def decodes(self, o): return TitledFloat(o) if o.ndim==0 else TitledTuple(o_.item() for o_ in o)\n",
    "    def setups(self, dsets):\n",
    "        if self.c is not None: return\n",
    "        try: self.c = len(dsets[0]) if hasattr(dsets[0], '__len__') else 1\n",
    "        except: self.c = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_tfm = RegressionSetup()\n",
    "dsets = Datasets([0, 1, 2], RegressionSetup)\n",
    "test_eq(dsets.c, 1)\n",
    "test_eq_type(dsets[0], (tensor(0.),))\n",
    "\n",
    "dsets = Datasets([[0, 1, 2], [3,4,5]], RegressionSetup)\n",
    "test_eq(dsets.c, 3)\n",
    "test_eq_type(dsets[0], (tensor([0.,1.,2.]),))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def get_c(dls):\n",
    "    if getattr(dls, 'c', False): return dls.c\n",
    "    if getattr(getattr(dls.train, 'after_item', None), 'c', False): return dls.train.after_item.c\n",
    "    if getattr(getattr(dls.train, 'after_batch', None), 'c', False): return dls.train.after_batch.c\n",
    "    vocab = getattr(dls, 'vocab', [])\n",
    "    if len(vocab) > 0 and is_listy(vocab[-1]): vocab = vocab[-1]\n",
    "    return len(vocab)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## End-to-end dataset example with MNIST"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's show how to use those functions to grab the mnist dataset in a `Datasets`. First we grab all the images."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "path = untar_data(URLs.MNIST_TINY)\n",
    "items = get_image_files(path)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then we split between train and validation depending on the folder."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((#3) [Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/9427.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/7043.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/train/3/9868.png')],\n",
       " (#3) [Path('/home/tcapelle/.fastai/data/mnist_tiny/valid/3/7024.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/valid/3/7431.png'),Path('/home/tcapelle/.fastai/data/mnist_tiny/valid/3/7233.png')])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "splitter = GrandparentSplitter()\n",
    "splits = splitter(items)\n",
    "train,valid = (items[i] for i in splits)\n",
    "train[:3],valid[:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our inputs are images that we open and convert to tensors, our targets are labeled depending on the parent directory and are categories."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from PIL import Image"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def open_img(fn:Path): return Image.open(fn).copy()\n",
    "def img2tensor(im:Image.Image): return TensorImage(array(im)[None])\n",
    "\n",
    "tfms = [[open_img, img2tensor],\n",
    "        [parent_label, Categorize()]]\n",
    "train_ds = Datasets(train, tfms)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x,y = train_ds[3]\n",
    "xd,yd = decode_at(train_ds,3)\n",
    "test_eq(parent_label(train[3]),yd)\n",
    "test_eq(array(Image.open(train[3])),xd[0].numpy())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "ax = show_at(train_ds, 3, cmap=\"Greys\", figsize=(1,1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "assert ax.title.get_text() in ('3','7')\n",
    "test_fig_exists(ax)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ToTensor -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class ToTensor(Transform):\n",
    "    \"Convert item to appropriate tensor class\"\n",
    "    order = 5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## IntToFloatTensor -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class IntToFloatTensor(DisplayedTransform):\n",
    "    \"Transform image to float tensor, optionally dividing by 255 (e.g. for images).\"\n",
    "    order = 10 #Need to run after PIL transforms on the GPU\n",
    "    def __init__(self, div=255., div_mask=1): store_attr()\n",
    "    def encodes(self, o:TensorImage): return o.float().div_(self.div)\n",
    "    def encodes(self, o:TensorMask ): return o.long() // self.div_mask\n",
    "    def decodes(self, o:TensorImage): return ((o.clamp(0., 1.) * self.div).long()) if self.div else o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = (TensorImage(tensor(1)),tensor(2).long(),TensorMask(tensor(3)))\n",
    "tfm = IntToFloatTensor()\n",
    "ft = tfm(t)\n",
    "test_eq(ft, [1./255, 2, 3])\n",
    "test_eq(type(ft[0]), TensorImage)\n",
    "test_eq(type(ft[2]), TensorMask)\n",
    "test_eq(ft[0].type(),'torch.FloatTensor')\n",
    "test_eq(ft[1].type(),'torch.LongTensor')\n",
    "test_eq(ft[2].type(),'torch.LongTensor')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Normalization -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "def broadcast_vec(dim, ndim, *t, cuda=True):\n",
    "    \"Make a vector broadcastable over `dim` (out of `ndim` total) by prepending and appending unit axes\"\n",
    "    v = [1]*ndim\n",
    "    v[dim] = -1\n",
    "    f = to_device if cuda else noop\n",
    "    return [f(tensor(o).view(*v)) for o in t]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "@docs\n",
    "class Normalize(DisplayedTransform):\n",
    "    \"Normalize/denorm batch of `TensorImage`\"\n",
    "    parameters,order = L('mean', 'std'),99\n",
    "    def __init__(self, mean=None, std=None, axes=(0,2,3)): store_attr()\n",
    "\n",
    "    @classmethod\n",
    "    def from_stats(cls, mean, std, dim=1, ndim=4, cuda=True): return cls(*broadcast_vec(dim, ndim, mean, std, cuda=cuda))\n",
    "\n",
    "    def setups(self, dl:DataLoader):\n",
    "        if self.mean is None or self.std is None:\n",
    "            x,*_ = dl.one_batch()\n",
    "            self.mean,self.std = x.mean(self.axes, keepdim=True),x.std(self.axes, keepdim=True)+1e-7\n",
    "\n",
    "    def encodes(self, x:TensorImage): return (x-self.mean) / self.std\n",
    "    def decodes(self, x:TensorImage):\n",
    "        f = to_cpu if x.device.type=='cpu' else noop\n",
    "        return (x*f(self.std) + f(self.mean))\n",
    "\n",
    "    _docs=dict(encodes=\"Normalize batch\", decodes=\"Denormalize batch\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mean,std = [0.5]*3,[0.5]*3\n",
    "mean,std = broadcast_vec(1, 4, mean, std)\n",
    "batch_tfms = [IntToFloatTensor(), Normalize.from_stats(mean,std)]\n",
    "tdl = TfmdDL(train_ds, after_batch=batch_tfms, bs=4, device=default_device())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x,y  = tdl.one_batch()\n",
    "xd,yd = tdl.decode((x,y))\n",
    "\n",
    "test_eq(x.type(), 'torch.cuda.FloatTensor' if default_device().type=='cuda' else 'torch.FloatTensor')\n",
    "test_eq(xd.type(), 'torch.LongTensor')\n",
    "test_eq(type(x), TensorImage)\n",
    "test_eq(type(y), TensorCategory)\n",
    "assert x.mean()<0.0\n",
    "assert x.std()>0.5\n",
    "assert 0<xd.float().mean()/255.<1\n",
    "assert 0<xd.float().std()/255.<0.5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "nrm = Normalize()\n",
    "batch_tfms = [IntToFloatTensor(), nrm]\n",
    "tdl = TfmdDL(train_ds, after_batch=batch_tfms, bs=4)\n",
    "x,y  = tdl.one_batch()\n",
    "test_close(x.mean(), 0.0, 1e-4)\n",
    "assert x.std()>0.9, x.std()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Just for visuals\n",
    "from fastai.vision.core import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "tdl.show_batch((x,y))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 72x72 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "#hide\n",
    "x,y = cast(x,Tensor),cast(y,Tensor) #Lose type of tensors (to emulate predictions)\n",
    "test_ne(type(x), TensorImage)\n",
    "tdl.show_batch((x,y), figsize=(1,1)) #Check that types are put back by dl."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Converted 00_torch_core.ipynb.\n",
      "Converted 01_layers.ipynb.\n",
      "Converted 01a_losses.ipynb.\n",
      "Converted 02_data.load.ipynb.\n",
      "Converted 03_data.core.ipynb.\n",
      "Converted 04_data.external.ipynb.\n",
      "Converted 05_data.transforms.ipynb.\n",
      "Converted 06_data.block.ipynb.\n",
      "Converted 07_vision.core.ipynb.\n",
      "Converted 08_vision.data.ipynb.\n",
      "Converted 09_vision.augment.ipynb.\n",
      "Converted 09b_vision.utils.ipynb.\n",
      "Converted 09c_vision.widgets.ipynb.\n",
      "Converted 10_tutorial.pets.ipynb.\n",
      "Converted 10b_tutorial.albumentations.ipynb.\n",
      "Converted 11_vision.models.xresnet.ipynb.\n",
      "Converted 12_optimizer.ipynb.\n",
      "Converted 13_callback.core.ipynb.\n",
      "Converted 13a_learner.ipynb.\n",
      "Converted 13b_metrics.ipynb.\n",
      "Converted 14_callback.schedule.ipynb.\n",
      "Converted 14a_callback.data.ipynb.\n",
      "Converted 15_callback.hook.ipynb.\n",
      "Converted 15a_vision.models.unet.ipynb.\n",
      "Converted 16_callback.progress.ipynb.\n",
      "Converted 17_callback.tracker.ipynb.\n",
      "Converted 18_callback.fp16.ipynb.\n",
      "Converted 18a_callback.training.ipynb.\n",
      "Converted 18b_callback.preds.ipynb.\n",
      "Converted 19_callback.mixup.ipynb.\n",
      "Converted 20_interpret.ipynb.\n",
      "Converted 20a_distributed.ipynb.\n",
      "Converted 21_vision.learner.ipynb.\n",
      "Converted 22_tutorial.imagenette.ipynb.\n",
      "Converted 23_tutorial.vision.ipynb.\n",
      "Converted 24_tutorial.image_sequence.ipynb.\n",
      "Converted 24_tutorial.siamese.ipynb.\n",
      "Converted 24_vision.gan.ipynb.\n",
      "Converted 30_text.core.ipynb.\n",
      "Converted 31_text.data.ipynb.\n",
      "Converted 32_text.models.awdlstm.ipynb.\n",
      "Converted 33_text.models.core.ipynb.\n",
      "Converted 34_callback.rnn.ipynb.\n",
      "Converted 35_tutorial.wikitext.ipynb.\n",
      "Converted 37_text.learner.ipynb.\n",
      "Converted 38_tutorial.text.ipynb.\n",
      "Converted 39_tutorial.transformers.ipynb.\n",
      "Converted 40_tabular.core.ipynb.\n",
      "Converted 41_tabular.data.ipynb.\n",
      "Converted 42_tabular.model.ipynb.\n",
      "Converted 43_tabular.learner.ipynb.\n",
      "Converted 44_tutorial.tabular.ipynb.\n",
      "Converted 45_collab.ipynb.\n",
      "Converted 46_tutorial.collab.ipynb.\n",
      "Converted 50_tutorial.datablock.ipynb.\n",
      "Converted 60_medical.imaging.ipynb.\n",
      "Converted 61_tutorial.medical_imaging.ipynb.\n",
      "Converted 65_medical.text.ipynb.\n",
      "Converted 70_callback.wandb.ipynb.\n",
      "Converted 71_callback.tensorboard.ipynb.\n",
      "Converted 72_callback.neptune.ipynb.\n",
      "Converted 73_callback.captum.ipynb.\n",
      "Converted 74_callback.azureml.ipynb.\n",
      "Converted 97_test_utils.ipynb.\n",
      "Converted 99_pytorch_doc.ipynb.\n",
      "Converted dev-setup.ipynb.\n",
      "Converted app_examples.ipynb.\n",
      "Converted camvid.ipynb.\n",
      "Converted migrating_catalyst.ipynb.\n",
      "Converted migrating_ignite.ipynb.\n",
      "Converted migrating_lightning.ipynb.\n",
      "Converted migrating_pytorch.ipynb.\n",
      "Converted ulmfit.ipynb.\n",
      "Converted index.ipynb.\n",
      "Converted quick_start.ipynb.\n",
      "Converted tutorial.ipynb.\n"
     ]
    }
   ],
   "source": [
    "#hide\n",
    "from nbdev.export import notebook2script\n",
    "notebook2script()"
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "split_at_heading": true
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
